/*
 * Copyright 2004 Rob Lambert (roblambert AT gmail DOT com)
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.zabada.util;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.opensymphony.oscache.base.NeedsRefreshException;
import com.opensymphony.oscache.general.GeneralCacheAdministrator;

/**
 * MethodInterceptor that looks for requested object in an OSCache
 * and returns it if found, if not found puts the returned object
 * from the interceptee into the OSCache and then returns it.
 * This assumes that the toString().toLowerCase() on each the arguments
 * of the intercepted methods can be considered unique as they are used to
 * to build the keys that will be used to store the Objects in the cache.
 */
public class OSCacheMethodInterceptor implements MethodInterceptor
{
  private final static Log log = LogFactory.getLog(OSCacheMethodInterceptor.class);
  private int cacheSeconds = 60 * 60 * 24 * 7;//DEFAULT 7 DAYS
  private boolean saveNullResult = true;
  private static GeneralCacheAdministrator cache = new GeneralCacheAdministrator();

  /**
   * Set time in seconds that returned object will stay active in the cache.  The default is 604800 seconds (7 days).
   * @param cacheSeconds
   */
  public void setCacheTimeSeconds(int cacheSeconds)
  {
    this.cacheSeconds = cacheSeconds;
  }

  /**
   * Set whether or not to save return value into cache if returned value is null.
   * Default is true which means that the null object will be saved into the cache,
   * and subsequent same calls will immediately return null without hitting the intercepted method.
   * Set to false if you want to retry the actual call when the result is null.  
   * @param saveNullResult
   */
  public void setSaveNullResult(boolean saveNullResult)
  {
    this.saveNullResult = saveNullResult; 
  }

  /**
   * The actual invoke method that intercepts and deals with caching.
   */
  public Object invoke(MethodInvocation invocation) throws Throwable
  {
    log.info("BEGINNING:" + invocation);
    //BUILD CACHE KEY USING METHOD NAME AND PARAMETERS
    StringBuffer cacheNameBuffer = new StringBuffer();
    cacheNameBuffer.append(invocation.getMethod().getName());
    if (invocation.getArguments() != null && invocation.getArguments().length > 0)
    {
      for (int a = 0; a < invocation.getArguments().length; a++)
      {
        cacheNameBuffer.append("-");
        if (invocation.getArguments()[a] == null)
          cacheNameBuffer.append("null");
        else
          cacheNameBuffer.append(invocation.getArguments()[a].toString().toLowerCase());
      }
    }
    if(log.isDebugEnabled())
      log.debug("cacheKey=" + cacheNameBuffer.toString());
    String cacheKey = cacheNameBuffer.toString();
    try
    {
      /*
      Retrieve from/Add to OSCache using GeneralCacheAdministrator
      "Typical use with fail over" as outlined in the javadoc at 
      http://www.opensymphony.com/oscache/api/com/opensymphony/oscache/general/GeneralCacheAdministrator.html
      */
      Object object = null;
      try
      {
        // Get from the cache
        if (log.isInfoEnabled())
          log.info("looking for " + cacheKey + " from the cache.");
        object = cache.getFromCache(cacheKey, cacheSeconds);
        if (log.isInfoEnabled())
          log.info("found " + cacheKey + " in the cache.");
      }
      catch (NeedsRefreshException nre)
      {
        //NOT IN CACHE, GET FROM INTERCEPTEE
        if (log.isInfoEnabled())
          log.info(cacheKey + " NOT found in the cache.");
        //OBJECT NOT IN CACHE
        //!!!! HERE COMES THE MOST IMPORTANT LINE !!!!!
        //PROCEED TO THE ACTUAL METHOD CALL AND GET
        //OBJECT FROM INTERCEPTEE
        object = invocation.proceed();
        //IF saveNullResult SETTING IS FALSE AND OBJECT IS NULL, CANCEL UPDATE OF CACHE
        if(object == null && !saveNullResult)
        {
          if (log.isInfoEnabled())
            log.info("objec is NULL, cancelling put into cache of " + cacheKey );

          // It is essential that cancelUpdate is called if the cached content is not rebuilt
          cache.cancelUpdate(cacheKey);
        }
        else
        {
          try
          {
            if (log.isInfoEnabled())
              log.info("putting " + cacheKey + " into the cache.");

            cache.putInCache(cacheKey, object);
          }
          catch (Exception ex)
          {
            // We have the current content if we want fail-over.
            object = nre.getCacheContent();
            if (log.isInfoEnabled())
              log.info("cancelling put into cache of " + cacheKey + ", exception: " + ex.getMessage());
            // It is essential that cancelUpdate is called if the cached content is not rebuilt
            cache.cancelUpdate(cacheKey);
          }
        }
      }
      return object;
    }
    finally
    {
      log.info("ENDING:" + invocation);
    }
  }
}